Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / View Controllers / OTRMessagesViewController.m @ f3813541

History | View | Annotate | Download (96.7 KB)

1
//
2
//  OTRMessagesViewController.m
3
//  Off the Record
4
//
5
//  Created by David Chiles on 5/12/14.
6
//  Copyright (c) 2014 Chris Ballinger. All rights reserved.
7
//
8

    
9
#import "OTRMessagesViewController.h"
10

    
11
#import "OTRDatabaseView.h"
12
#import "OTRDatabaseManager.h"
13
#import "OTRLog.h"
14

    
15
#import "OTRBuddy.h"
16
#import "OTRAccount.h"
17
#import "OTRMessage+JSQMessageData.h"
18
@import JSQMessagesViewController;
19
@import MobileCoreServices;
20
#import "OTRProtocolManager.h"
21
#import "OTRXMPPTorAccount.h"
22
#import "OTRXMPPManager.h"
23
#import "OTRLockButton.h"
24
#import "OTRButtonView.h"
25
@import OTRAssets;
26
#import "OTRTitleSubtitleView.h"
27
@import OTRKit;
28
@import FormatterKit;
29
#import "OTRImages.h"
30
#import "UIActivityViewController+ChatSecure.h"
31
#import "OTRUtilities.h"
32
#import "OTRProtocolManager.h"
33
#import "OTRColors.h"
34
#import "JSQMessagesCollectionViewCell+ChatSecure.h"
35
@import BButton;
36
#import "OTRAttachmentPicker.h"
37
#import "OTRImageItem.h"
38
#import "OTRVideoItem.h"
39
#import "OTRAudioItem.h"
40
@import JTSImageViewController;
41
#import "OTRAudioControlsView.h"
42
#import "OTRPlayPauseProgressView.h"
43
#import "OTRAudioPlaybackController.h"
44
#import "OTRMediaFileManager.h"
45
#import "OTRMediaServer.h"
46
#import "UIImage+ChatSecure.h"
47
#import "OTRBaseLoginViewController.h"
48

    
49
#import <ChatSecureCore/ChatSecureCore-Swift.h>
50
#import "OTRYapMessageSendAction.h"
51
#import "UIViewController+ChatSecure.h"
52
#import "OTRBuddyCache.h"
53
#import "OTRTextItem.h"
54
#import "OTRHTMLItem.h"
55
#import "OTRFileItem.h"
56
@import YapDatabase;
57
@import PureLayout;
58
@import KVOController;
59

    
60
@import AVFoundation;
61
@import MediaPlayer;
62

    
63
static NSTimeInterval const kOTRMessageSentDateShowTimeInterval = 5 * 60;
64
static NSUInteger const kOTRMessagePageSize = 50;
65

    
66
typedef NS_ENUM(int, OTRDropDownType) {
67
    OTRDropDownTypeNone          = 0,
68
    OTRDropDownTypeEncryption    = 1,
69
    OTRDropDownTypePush          = 2
70
};
71

    
72
@interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol, OTRRoomOccupantsViewControllerDelegate> {
73
    JSQMessagesAvatarImage *_warningAvatarImage;
74
    JSQMessagesAvatarImage *_accountAvatarImage;
75
    JSQMessagesAvatarImage *_buddyAvatarImage;
76
}
77

    
78
@property (nonatomic, strong) OTRYapViewHandler *viewHandler;
79

    
80
@property (nonatomic, strong) JSQMessagesBubbleImage *outgoingBubbleImage;
81
@property (nonatomic, strong) JSQMessagesBubbleImage *incomingBubbleImage;
82

    
83
@property (nonatomic, weak) id didFinishGeneratingPrivateKeyNotificationObject;
84
@property (nonatomic, weak) id messageStateDidChangeNotificationObject;
85
@property (nonatomic, weak) id pendingApprovalDidChangeNotificationObject;
86
@property (nonatomic, weak) id deviceListUpdateNotificationObject;
87

    
88

    
89
@property (nonatomic ,strong) UIBarButtonItem *lockBarButtonItem;
90
@property (nonatomic, strong) OTRLockButton *lockButton;
91
@property (nonatomic, strong) OTRButtonView *buttonDropdownView;
92

    
93
@property (nonatomic, strong) OTRAttachmentPicker *attachmentPicker;
94
@property (nonatomic, strong) OTRAudioPlaybackController *audioPlaybackController;
95

    
96
@property (nonatomic, strong) NSTimer *lastSeenRefreshTimer;
97
@property (nonatomic, strong) UIView *jidForwardingHeaderView;
98

    
99
@property (nonatomic) BOOL loadingMessages;
100
@property (nonatomic) BOOL messageRangeExtended;
101
@property (nonatomic, strong) NSIndexPath *currentIndexPath;
102
@property (nonatomic, strong) id currentMessage;
103
@property (nonatomic, strong) NSCache *messageSizeCache;
104

    
105
@end
106

    
107
@implementation OTRMessagesViewController
108

    
109
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
110
{
111
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
112
        self.senderId = @"";
113
        self.senderDisplayName = @"";
114
        _state = [[MessagesViewControllerState alloc] init];
115
        self.messageSizeCache = [NSCache new];
116
        self.messageSizeCache.countLimit = kOTRMessagePageSize;
117
        self.messageRangeExtended = NO;
118
    }
119
    return self;
120
}
121

    
122
#pragma - mark Lifecylce Methods
123

    
124
- (void) dealloc {
125
    [self.lastSeenRefreshTimer invalidate];
126
    [[NSNotificationCenter defaultCenter] removeObserver:self];
127
}
128

    
129
- (void)viewDidLoad
130
{
131
    [super viewDidLoad];
132
    
133
    self.automaticallyScrollsToMostRecentMessage = YES;
134
    
135
     ////// bubbles //////
136
    JSQMessagesBubbleImageFactory *bubbleImageFactory = [[JSQMessagesBubbleImageFactory alloc] init];
137
                                                         
138
    self.outgoingBubbleImage = [bubbleImageFactory outgoingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleBlueColor]];
139
    
140
    self.incomingBubbleImage = [bubbleImageFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
141
    
142
    ////// TitleView //////
143
    OTRTitleSubtitleView *titleView = [self titleView];
144
    [self refreshTitleView:titleView];
145
    self.navigationItem.titleView = titleView;
146
    
147
    ////// Send Button //////
148
    self.sendButton = [JSQMessagesToolbarButtonFactory defaultSendButtonItem];
149
    
150
    ////// Attachment Button //////
151
    self.inputToolbar.contentView.leftBarButtonItem = nil;
152
    self.cameraButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
153
    self.cameraButton.titleLabel.font = [UIFont fontWithName:kFontAwesomeFont size:20];
154
    self.cameraButton.titleLabel.textAlignment = NSTextAlignmentCenter;
155
    [self.cameraButton setTitle:[NSString fa_stringForFontAwesomeIcon:FACamera] forState:UIControlStateNormal];
156
    self.cameraButton.frame = CGRectMake(0, 0, 32, 32);
157
    
158
    ////// Microphone Button //////
159
    self.microphoneButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
160
    self.microphoneButton.frame = CGRectMake(0, 0, 32, 32);
161
    self.microphoneButton.titleLabel.font = [UIFont fontWithName:kFontAwesomeFont size:20];
162
    self.microphoneButton.titleLabel.textAlignment = NSTextAlignmentCenter;
163
    [self.microphoneButton setTitle:[NSString fa_stringForFontAwesomeIcon:FAMicrophone]
164
          forState:UIControlStateNormal];
165
    
166
    self.audioPlaybackController = [[OTRAudioPlaybackController alloc] init];
167
    
168
    ////// TextViewUpdates //////
169
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedTextViewChangedNotification:) name:UITextViewTextDidChangeNotification object:self.inputToolbar.contentView.textView];
170
    
171
    /** Setup databse view handler*/
172
    self.viewHandler = [[OTRYapViewHandler alloc] initWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection databaseChangeNotificationName:[DatabaseNotificationName LongLivedTransactionChanges]];
173
    self.viewHandler.delegate = self;
174
    
175
    ///Custom Layout to account for no bubble cells
176
    OTRMessagesCollectionViewFlowLayout *layout = [[OTRMessagesCollectionViewFlowLayout alloc] init];
177
    layout.sizeDelegate = self;
178
    self.collectionView.collectionViewLayout = layout;
179

    
180
    ///"Loading Earlier" header view
181
    [self.collectionView registerNib:[UINib nibWithNibName:@"OTRMessagesLoadingView" bundle:OTRAssets.resourcesBundle]
182
          forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
183
                 withReuseIdentifier:[JSQMessagesLoadEarlierHeaderView headerReuseIdentifier]];
184

    
185
    //Subscribe to changes in encryption state
186
    __weak typeof(self)weakSelf = self;
187
    [self.KVOController observe:self.state keyPath:NSStringFromSelector(@selector(messageSecurity)) options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
188
        __typeof__(self) strongSelf = weakSelf;
189
        if (!strongSelf) { return; }
190
        
191
        if ([object isKindOfClass:[MessagesViewControllerState class]]) {
192
            MessagesViewControllerState *state = (MessagesViewControllerState*)object;
193
            NSString * placeHolderString = nil;
194
            switch (state.messageSecurity) {
195
                case OTRMessageTransportSecurityPlaintext:
196
                case OTRMessageTransportSecurityPlaintextWithOTR:
197
                    placeHolderString = SEND_PLAINTEXT_STRING();
198
                    break;
199
                case OTRMessageTransportSecurityOTR:
200
                    placeHolderString = [NSString stringWithFormat:SEND_ENCRYPTED_STRING(),@"OTR"];
201
                    break;
202
                case OTRMessageTransportSecurityOMEMO:
203
                    placeHolderString = [NSString stringWithFormat:SEND_ENCRYPTED_STRING(),@"OMEMO"];;
204
                    break;
205
                    
206
                default:
207
                    placeHolderString = [NSBundle jsq_localizedStringForKey:@"new_message"];
208
                    break;
209
            }
210
            strongSelf.inputToolbar.contentView.textView.placeHolder = placeHolderString;
211
            [self didUpdateState];
212
        }
213
    }];
214
    
215
}
216

    
217
- (void)viewDidAppear:(BOOL)animated
218
{
219
    [super viewDidAppear:animated];
220
    [self tryToMarkAllMessagesAsRead];
221
    // This is a hack to attempt fixing https://github.com/ChatSecure/ChatSecure-iOS/issues/657
222
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
223
        [self scrollToBottomAnimated:animated];
224
    });
225
    self.loadingMessages = NO;
226
}
227

    
228
- (void)viewWillAppear:(BOOL)animated
229
{
230
    self.currentIndexPath = nil;
231
    
232
    [super viewWillAppear:animated];
233
    [[UIApplication sharedApplication] setStatusBarHidden:NO];
234
    
235
    if (self.lastSeenRefreshTimer) {
236
        [self.lastSeenRefreshTimer invalidate];
237
        _lastSeenRefreshTimer = nil;
238
    }
239
    _lastSeenRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(refreshTitleTimerUpdate:) userInfo:nil repeats:YES];
240
    
241
    __weak typeof(self)weakSelf = self;
242
    void (^refreshGeneratingLock)(OTRAccount *) = ^void(OTRAccount * account) {
243
        __strong typeof(weakSelf)strongSelf = weakSelf;
244
        __block NSString *accountKey = nil;
245
        [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
246
            accountKey = [strongSelf buddyWithTransaction:transaction].accountUniqueId;
247
        }];
248
        if ([account.uniqueId isEqualToString:accountKey]) {
249
            [strongSelf updateEncryptionState];
250
        }
251
        
252
        
253
    };
254
    
255
    self.didFinishGeneratingPrivateKeyNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRDidFinishGeneratingPrivateKeyNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
256
        if ([note.object isKindOfClass:[OTRAccount class]]) {
257
            refreshGeneratingLock(note.object);
258
        }
259
    }];
260
   
261
    self.messageStateDidChangeNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRMessageStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
262
        __strong typeof(weakSelf)strongSelf = weakSelf;
263
        if ([note.object isKindOfClass:[OTRBuddy class]]) {
264
            OTRBuddy *notificationBuddy = note.object;
265
            __block NSString *buddyKey = nil;
266
            [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
267
                buddyKey = [strongSelf buddyWithTransaction:transaction].uniqueId;
268
            }];
269
            if ([notificationBuddy.uniqueId isEqualToString:buddyKey]) {
270
                [strongSelf updateEncryptionState];
271
            }
272
        }
273
    }];
274
    
275
    if ([self.threadKey length]) {
276
        [self.viewHandler.keyCollectionObserver observe:self.threadKey collection:self.threadCollection];
277
        [self updateViewWithKey:self.threadKey collection:self.threadCollection];
278
        [self.viewHandler setup:OTRFilteredChatDatabaseViewExtensionName groups:@[self.threadKey]];
279
        if(![self.inputToolbar.contentView.textView.text length]) {
280
            [self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView];
281
        }
282
    }
283

    
284
    self.loadingMessages = YES;
285
    [self.collectionView reloadData];
286
}
287

    
288
- (void)viewWillDisappear:(BOOL)animated
289
{
290
    [super viewWillDisappear:animated];
291
    
292
    [self.lastSeenRefreshTimer invalidate];
293
    self.lastSeenRefreshTimer = nil;
294
    
295
    [self saveCurrentMessageText:self.inputToolbar.contentView.textView.text threadKey:self.threadKey colleciton:self.threadCollection];
296
    
297
    [[NSNotificationCenter defaultCenter] removeObserver:self.messageStateDidChangeNotificationObject];
298
    [[NSNotificationCenter defaultCenter] removeObserver:self.didFinishGeneratingPrivateKeyNotificationObject];
299
    
300
    // [self.inputToolbar.contentView.textView resignFirstResponder];
301
}
302

    
303
- (void)viewDidDisappear:(BOOL)animated
304
{
305
    [super viewDidDisappear:animated];
306

    
307
    _warningAvatarImage = nil;
308
    _accountAvatarImage = nil;
309
    _buddyAvatarImage = nil;
310
}
311

    
312
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
313
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
314
    
315
    // After the transition is done, we need to reset the size caches and relayout
316
    // Do this using the technique in https://stackoverflow.com/questions/26943808/ios-how-to-run-a-function-after-device-has-rotated-swift
317
    [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
318
        [self.messageSizeCache removeAllObjects];
319
        [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
320
    }];
321
}
322

    
323
#pragma - mark Setters & getters
324

    
325
- (OTRAttachmentPicker *)attachmentPicker
326
{
327
    if (!_attachmentPicker) {
328
        _attachmentPicker = [[OTRAttachmentPicker alloc] initWithParentViewController:self delegate:self];
329
    }
330
    return _attachmentPicker;
331
}
332

    
333
- (NSArray*) indexPathsToCount:(NSUInteger)count {
334
    NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:count];
335
    for (NSUInteger i = 0; i < count; i++) {
336
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
337
        [indexPaths addObject:indexPath];
338
    }
339
    return indexPaths;
340
}
341

    
342
- (nullable id<OTRThreadOwner>)threadObjectWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
343
    if (!self.threadKey || !self.threadCollection || !transaction) { return nil; }
344
    id object = [transaction objectForKey:self.threadKey inCollection:self.threadCollection];
345
    if ([object conformsToProtocol:@protocol(OTRThreadOwner)]) {
346
        return object;
347
    }
348
    return nil;
349
}
350

    
351
- (nullable OTRBuddy *)buddyWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
352
    id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction];
353
    if ([object isKindOfClass:[OTRBuddy class]]) {
354
        return (OTRBuddy *)object;
355
    }
356
    return nil;
357
}
358

    
359
- (nullable OTRXMPPRoom *)roomWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
360
    id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction];
361
    if ([object isKindOfClass:[OTRXMPPRoom class]]) {
362
        return (OTRXMPPRoom *)object;
363
    }
364
    return nil;
365
}
366

    
367
- (nullable OTRAccount *)accountWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
368
    id <OTRThreadOwner> thread =  [self threadObjectWithTransaction:transaction];
369
    if (!thread) { return nil; }
370
    OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction];
371
    return account;
372
}
373

    
374
- (void)setThreadKey:(NSString *)key collection:(NSString *)collection
375
{
376
    self.currentIndexPath = nil;
377
    NSString *oldKey = self.threadKey;
378
    NSString *oldCollection = self.threadCollection;
379
    
380
    self.threadKey = key;
381
    self.threadCollection = collection;
382
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
383
        self.senderId = [[self threadObjectWithTransaction:transaction] threadAccountIdentifier];
384
    }];
385
    
386
    // Clear out old state (don't just alloc a new object, we have KVOs attached to this!)
387
    self.state.canSendMedia = NO;
388
    self.state.canKnock = NO;
389
    self.state.messageSecurity = OTRMessageTransportSecurityInvalid;
390
    self.state.hasText = NO;
391
    self.state.isThreadOnline = NO;
392
    self.showTypingIndicator = NO;
393
    
394
    // This is set to nil so the refreshTitleView: method knows to reset username instead of last seen time
395
    [self titleView].subtitleLabel.text = nil;
396
    
397
    if (![oldKey isEqualToString:key] || ![oldCollection isEqualToString:collection]) {
398
        [self saveCurrentMessageText:self.inputToolbar.contentView.textView.text threadKey:oldKey colleciton:oldCollection];
399
        self.inputToolbar.contentView.textView.text = nil;
400
        [self receivedTextViewChanged:self.inputToolbar.contentView.textView];
401
    }
402
    
403
    [self.viewHandler.keyCollectionObserver stopObserving:oldKey collection:oldCollection];
404
    if (self.threadKey && self.threadCollection) {
405
        [self.viewHandler.keyCollectionObserver observe:self.threadKey collection:self.threadCollection];
406
        [self updateViewWithKey:self.threadKey collection:self.threadCollection];
407
        [self.viewHandler setup:OTRFilteredChatDatabaseViewExtensionName groups:@[self.threadKey]];
408
        [self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView];
409
    } else {
410
        // Reset the view handler
411
        self.viewHandler = [[OTRYapViewHandler alloc] initWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection databaseChangeNotificationName:[DatabaseNotificationName LongLivedTransactionChanges]];
412
        self.viewHandler.delegate = self;
413
        self.senderDisplayName = @"";
414
        self.senderId = @"";
415
    }
416
    
417
    [self.collectionView reloadData];
418
    
419
    // Profile Info Button
420
    [self setupInfoButton];
421
    
422
    [self updateEncryptionState];
423
    [self updateJIDForwardingHeader];
424
    
425
    __weak typeof(self)weakSelf = self;
426
    if (self.pendingApprovalDidChangeNotificationObject == nil) {
427
        self.pendingApprovalDidChangeNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRBuddyPendingApprovalDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
428
            __strong typeof(weakSelf)strongSelf = weakSelf;
429
            OTRXMPPBuddy *notificationBuddy = [note.userInfo objectForKey:@"buddy"];
430
            __block NSString *buddyKey = nil;
431
            [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
432
                buddyKey = [strongSelf buddyWithTransaction:transaction].uniqueId;
433
            }];
434
            if ([notificationBuddy.uniqueId isEqualToString:buddyKey]) {
435
                [strongSelf fetchOMEMODeviceList];
436
                [strongSelf sendPresenceProbe];
437
            }
438
        }];
439
    }
440
    
441
    if (self.deviceListUpdateNotificationObject == nil) {
442
        self.deviceListUpdateNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTROMEMOSignalCoordinator.DeviceListUpdateNotificationName object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
443
            __strong typeof(weakSelf)strongSelf = weakSelf;
444
            XMPPJID *notificationJid = [note.userInfo objectForKey:@"jid"];
445
            __block NSString *buddyUser = nil;
446
            [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
447
                buddyUser = [strongSelf buddyWithTransaction:transaction].username;
448
            }];
449
            if (notificationJid != nil && [notificationJid.bare isEqualToString:buddyUser]) {
450
                [strongSelf updateEncryptionState];
451
            }
452
        }];
453
    }
454
    
455
    [self sendPresenceProbe];
456
    [self fetchOMEMODeviceList];
457
}
458

    
459
                           
460
- (YapDatabaseConnection *)readOnlyDatabaseConnection
461
{
462
    if (!_readOnlyDatabaseConnection) {
463
        _readOnlyDatabaseConnection = [OTRDatabaseManager sharedInstance].readOnlyDatabaseConnection;
464
    }
465
    return _readOnlyDatabaseConnection;
466
}
467
                           
468
- (YapDatabaseConnection *)readWriteDatabaseConnection
469
{
470
    if (!_readWriteDatabaseConnection) {
471
            _readWriteDatabaseConnection = [OTRDatabaseManager sharedInstance].readWriteDatabaseConnection;
472
    }
473
    return _readWriteDatabaseConnection;
474
}
475
                        
476

    
477
- (nullable OTRXMPPManager *)xmppManagerWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
478
    OTRAccount *account = [self accountWithTransaction:transaction];
479
    if (!account) { return nil; }
480
    return (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
481
}
482

    
483
/** Will send a probe to fetch last seen */
484
- (void) sendPresenceProbe {
485
    __block OTRXMPPManager *xmpp = nil;
486
    __block OTRXMPPBuddy *buddy = nil;
487
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
488
        xmpp = [self xmppManagerWithTransaction:transaction];
489
        buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction];
490
    }];
491
    if (!xmpp || ![buddy isKindOfClass:[OTRXMPPBuddy class]] || buddy.pendingApproval) { return; }
492
    [xmpp sendPresenceProbeForBuddy:buddy];
493
}
494

    
495
- (void)updateViewWithKey:(NSString *)key collection:(NSString *)collection
496
{
497
    if ([collection isEqualToString:[OTRBuddy collection]]) {
498
        __block OTRBuddy *buddy = nil;
499
        __block OTRAccount *account = nil;
500
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
501
            buddy = [OTRBuddy fetchObjectWithUniqueID:key transaction:transaction];
502
            account = [OTRAccount fetchObjectWithUniqueID:buddy.accountUniqueId transaction:transaction];
503
        }];
504
        
505
        
506
        
507
        //Update UI now
508
        if (buddy.chatState == OTRChatStateComposing || buddy.chatState == OTRChatStatePaused) {
509
            self.showTypingIndicator = YES;
510
        }
511
        else {
512
            self.showTypingIndicator = NO;
513
        }
514
        
515
        // Update Buddy Status
516
        BOOL previousState = self.state.isThreadOnline;
517
        self.state.isThreadOnline = buddy.status != OTRThreadStatusOffline;
518
        
519
        [self didUpdateState];
520
        
521
        //Update Buddy knock status
522
        //Async because this calls down to the database and iterates over a relation. Might slowdown the UI if on main thread
523
        __weak __typeof__(self) weakSelf = self;
524
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
525
            __typeof__(self) strongSelf = weakSelf;
526
            __block BOOL canKnock = [[[OTRProtocolManager sharedInstance].pushController pushStorage] numberOfTokensForBuddy:buddy.uniqueId createdByThisAccount:NO] > 0;
527
            dispatch_async(dispatch_get_main_queue(), ^{
528
                if (canKnock != strongSelf.state.canKnock) {
529
                    strongSelf.state.canKnock = canKnock;
530
                    [strongSelf didUpdateState];
531
                }
532
            });
533
            
534
        });
535
        
536
        [self refreshTitleView:[self titleView]];
537

    
538
        // Auto-inititate OTR when contact comes online
539
        if (!previousState && self.state.isThreadOnline) {
540
            [[OTRProtocolManager sharedInstance].encryptionManager maybeRefreshOTRSessionForBuddyKey:key collection:collection];
541
        }
542
    } else if ([collection isEqualToString:[OTRXMPPRoom collection]]) {
543
        __block OTRXMPPRoom *room = nil;
544
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
545
            room = [OTRXMPPRoom fetchObjectWithUniqueID:key transaction:transaction];
546
        }];
547
        self.state.isThreadOnline = room.currentStatus != OTRThreadStatusOffline;
548
        [self didUpdateState];
549
        [self refreshTitleView:[self titleView]];
550
    }
551
    [self tryToMarkAllMessagesAsRead];
552
}
553

    
554
- (void)tryToMarkAllMessagesAsRead {
555
    // Set all messages as read
556
    if ([self otr_isVisible]) {
557
        __weak __typeof__(self) weakSelf = self;
558
        __block id <OTRThreadOwner>threadOwner = nil;
559
        __block NSArray <id <OTRMessageProtocol>>* unreadMessages = nil;
560
        [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
561
            threadOwner = [weakSelf threadObjectWithTransaction:transaction];
562
            if (!threadOwner) { return; }
563
            unreadMessages = [transaction allUnreadMessagesForThread:threadOwner];
564
        } completionBlock:^{
565
            
566
            if ([unreadMessages count] == 0) {
567
                return;
568
            }
569
            
570
            //Mark as read
571
            
572
            NSMutableArray <id <OTRMessageProtocol>>*toBeSaved = [[NSMutableArray alloc] init];
573
            
574
            [unreadMessages enumerateObjectsUsingBlock:^(id<OTRMessageProtocol>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
575
                if ([obj isKindOfClass:[OTRIncomingMessage class]]) {
576
                    OTRIncomingMessage *message = [((OTRIncomingMessage *)obj) copy];
577
                    message.read = YES;
578
                    [toBeSaved addObject:message];
579
                } else if ([obj isKindOfClass:[OTRXMPPRoomMessage class]]) {
580
                    OTRXMPPRoomMessage *message = [((OTRXMPPRoomMessage *)obj) copy];
581
                    message.read = YES;
582
                    [toBeSaved addObject:message];
583
                }
584
            }];
585
            
586
            [weakSelf.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
587
                [toBeSaved enumerateObjectsUsingBlock:^(id<OTRMessageProtocol>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
588
                    [transaction setObject:obj forKey:[obj messageKey] inCollection:[obj messageCollection]];
589
                }];
590
                [transaction touchObjectForKey:[threadOwner threadIdentifier] inCollection:[threadOwner threadCollection]];
591
            }];
592
        }];
593
    }
594
}
595

    
596
- (OTRTitleSubtitleView * __nonnull)titleView {
597
    UIView *titleView = self.navigationItem.titleView;
598
    if ([titleView isKindOfClass:[OTRTitleSubtitleView class]]) {
599
        return  (OTRTitleSubtitleView*)titleView;
600
    }
601
    return [[OTRTitleSubtitleView alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
602
}
603

    
604
- (void)refreshTitleTimerUpdate:(NSTimer*)timer {
605
    [self refreshTitleView:[self titleView]];
606
}
607

    
608
/** Updates the title view with the current thread information on this view controller*/
609
- (void)refreshTitleView:(OTRTitleSubtitleView *)titleView
610
{
611
    __block id<OTRThreadOwner> thread = nil;
612
    __block OTRAccount *account = nil;
613
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
614
        thread = [self threadObjectWithTransaction:transaction];
615
        account =  [self accountWithTransaction:transaction];
616
    }];
617
    
618
    titleView.titleLabel.text = [thread threadName];
619
    
620
    UIImage *statusImage = nil;
621
    if ([thread isKindOfClass:[OTRBuddy class]]) {
622
        OTRBuddy *buddy = (OTRBuddy*)thread;
623
        UIColor *color = [buddy avatarBorderColor];
624
        if (color) { // only show online status
625
            statusImage = [OTRImages circleWithRadius:50
626
                                      lineWidth:0
627
                                      lineColor:nil
628
                                      fillColor:color];
629
        }
630
        
631
        dispatch_block_t refreshTimeBlock = ^{
632
            __block OTRBuddy *buddy = nil;
633
            [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
634
                buddy = (OTRBuddy*)[self threadObjectWithTransaction:transaction];
635
            }];
636
            if (![buddy isKindOfClass:[OTRBuddy class]]) {
637
                return;
638
            }
639
            NSDate *lastSeen = [OTRBuddyCache.shared lastSeenDateForBuddy:buddy];
640
            OTRThreadStatus status = [OTRBuddyCache.shared threadStatusForBuddy:buddy];
641
            if (!lastSeen) {
642
                titleView.subtitleLabel.text = buddy.username;
643
                return;
644
            }
645
            TTTTimeIntervalFormatter *tf = [[TTTTimeIntervalFormatter alloc] init];
646
            tf.presentTimeIntervalMargin = 60;
647
            tf.usesAbbreviatedCalendarUnits = YES;
648
            NSTimeInterval lastSeenInterval = [lastSeen timeIntervalSinceDate:[NSDate date]];
649
            NSString *labelString = nil;
650
            if (status == OTRThreadStatusAvailable) {
651
                labelString = buddy.username;
652
            } else {
653
                labelString = [NSString stringWithFormat:@"%@ %@", ACTIVE_STRING(), [tf stringForTimeInterval:lastSeenInterval]];
654
            }
655
            titleView.subtitleLabel.text = labelString;
656
        };
657
        
658
        // Set the username if nothing else is set.
659
        // This should be cleared out when buddy is changed
660
        if (!titleView.subtitleLabel.text) {
661
            titleView.subtitleLabel.text = buddy.username;
662
        }
663
        
664
        // Show an "Last seen 11 min ago" in title bar after brief delay
665
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
666
            refreshTimeBlock();
667
        });
668
    } else if ([thread isGroupThread]) {
669
        titleView.subtitleLabel.text = GROUP_CHAT_STRING();
670
    } else {
671
        titleView.subtitleLabel.text = nil;
672
    }
673
    
674
    titleView.titleImageView.image = statusImage;
675

    
676
}
677

    
678
/**
679
 This generates a UIAlertAction where the handler fetches the outgoing message (optionaly duplicates). Then if media message resend media message. If not update messageSecurityInfo and date and create new sending action.
680
 */
681
- (UIAlertAction *)resendOutgoingMessageActionForMessageKey:(NSString *)messageKey
682
                                          messageCollection:(NSString *)messageCollection
683
                                readWriteDatabaseConnection:(YapDatabaseConnection*)databaseConnection
684
                                                      title:(NSString *)title
685
{
686
    UIAlertAction *action = [UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
687
        [databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
688
            id object = [[transaction objectForKey:messageKey inCollection:messageCollection] copy];
689
            id<OTRMessageProtocol> message = nil;
690
            if ([object conformsToProtocol:@protocol(OTRMessageProtocol)]) {
691
                message = (id<OTRMessageProtocol>)object;
692
            } else {
693
                return;
694
            }
695
            // Messages that never sent properly don't need to be duplicated client-side
696
            NSError *messageError = message.messageError;
697
            message = [message duplicateMessage];
698
            message.messageError = nil;
699
            message.messageSecurity = self.state.messageSecurity;
700
            message.messageDate = [NSDate date];
701
            [message saveWithTransaction:transaction];
702
            
703
            // We only need to re-upload failed media messages
704
            // otherwise just resend the URL directly
705
            if (message.messageMediaItemKey.length &&
706
                (!message.messageText.length || messageError)) {
707
                OTRMediaItem *mediaItem = [OTRMediaItem fetchObjectWithUniqueID:message.messageMediaItemKey transaction:transaction];
708
                [self sendMediaItem:mediaItem data:nil message:message transaction:transaction];
709
            } else {
710
                OTRYapMessageSendAction *sendingAction = [OTRYapMessageSendAction sendActionForMessage:message date:message.messageDate];
711
                [sendingAction saveWithTransaction:transaction];
712
            }
713
        }];
714
    }];
715
    return action;
716
}
717

    
718
- (nonnull UIAlertAction *)viewProfileAction {
719
    return [UIAlertAction actionWithTitle:VIEW_PROFILE_STRING() style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
720
        [self infoButtonPressed:action];
721
    }];
722
}
723

    
724
- (nonnull UIAlertAction *)cancleAction {
725
    return [UIAlertAction actionWithTitle:CANCEL_STRING()
726
                                    style:UIAlertActionStyleCancel
727
                                  handler:nil];
728
}
729

    
730
- (NSArray <UIAlertAction *>*)actionForMessage:(id<OTRMessageProtocol>)message {
731
    NSMutableArray <UIAlertAction *>*actions = [[NSMutableArray alloc] init];
732
    
733
    if (!message.isMessageIncoming) {
734
        // This is an outgoing message so we can offer to resend
735
        UIAlertAction *resendAction = [self resendOutgoingMessageActionForMessageKey:message.messageKey messageCollection:message.messageCollection readWriteDatabaseConnection:self.readWriteDatabaseConnection  title:RESEND_STRING()];
736
        [actions addObject:resendAction];
737
    }
738
    
739
    if (![message isKindOfClass:[OTRXMPPRoomMessage class]]) {
740
        [actions addObject:[self viewProfileAction]];
741
    }
742
    
743
    NSArray<UIAlertAction*> *mediaActions = [UIAlertAction actionsForMediaMessage:message sourceView:self.view viewController:self];
744
    [actions addObjectsFromArray:mediaActions];
745
    
746
    [actions addObject:[self cancleAction]];
747
    return actions;
748
}
749

    
750
- (void)didTapAvatar:(id<OTRMessageProtocol>)message sender:(id)sender {
751
    NSError *error =  [message messageError];
752
    NSString *title = nil;
753
    NSString *alertMessage = nil;
754
    
755
    NSString * sendingType = UNENCRYPTED_STRING();
756
    switch (self.state.messageSecurity) {
757
        case OTRMessageTransportSecurityOTR:
758
            sendingType = @"OTR";
759
            break;
760
        case OTRMessageTransportSecurityOMEMO:
761
            sendingType = @"OMEMO";
762
            break;
763
            
764
        default:
765
            break;
766
    }
767
    
768
    if ([message isKindOfClass:[OTROutgoingMessage class]]) {
769
        title = RESEND_MESSAGE_TITLE();
770
        alertMessage = [NSString stringWithFormat:RESEND_DESCRIPTION_STRING(),sendingType];
771
    }
772
    
773
    if (error) {
774
        NSUInteger otrFingerprintError = 32872;
775
        title = ERROR_STRING();
776
        alertMessage = error.localizedDescription;
777
        
778
        if (error.code == otrFingerprintError) {
779
            alertMessage = NO_DEVICES_BUDDY_ERROR_STRING();
780
        }
781
        
782
        if([message isKindOfClass:[OTROutgoingMessage class]]) {
783
            //If it's an outgoing message the error title should be that we were unable to send the message.
784
            title = UNABLE_TO_SEND_STRING();
785
            
786
            
787
            
788
            NSString *resendDescription = [NSString stringWithFormat:RESEND_DESCRIPTION_STRING(),sendingType];
789
            alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",resendDescription]];
790
            
791
            //If this is an error about not having a trusted identity then we should offer to connect to the
792
            if (error.code == OTROMEMOErrorNoDevicesForBuddy ||
793
                error.code == OTROMEMOErrorNoDevices ||
794
                error.code == otrFingerprintError) {
795
                
796
                alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",VIEW_PROFILE_DESCRIPTION_STRING()]];
797
            }
798
        }
799
    }
800
    
801
    
802
    if (![self isMessageTrusted:message]) {
803
        title = UNTRUSTED_DEVICE_STRING();
804
        if ([message isMessageIncoming]) {
805
            alertMessage = UNTRUSTED_DEVICE_REVEIVED_STRING();
806
        } else {
807
            alertMessage = UNTRUSTED_DEVICE_SENT_STRING();
808
        }
809
        alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",VIEW_PROFILE_DESCRIPTION_STRING()]];
810
    }
811
    
812
    NSArray <UIAlertAction*>*actions = [self actionForMessage:message];
813
    if ([actions count] > 0) {
814
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:alertMessage preferredStyle:UIAlertControllerStyleActionSheet];
815
        [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
816
            [alertController addAction:obj];
817
        }];
818
        if ([sender isKindOfClass:[UIView class]]) {
819
            UIView *sourceView = sender;
820
            alertController.popoverPresentationController.sourceView = sourceView;
821
            alertController.popoverPresentationController.sourceRect = sourceView.bounds;
822
        }
823
        [self presentViewController:alertController animated:YES completion:nil];
824
    }
825
}
826

    
827
- (BOOL)isMessageTrusted:(id <OTRMessageProtocol>)message {
828
    BOOL trusted = YES;
829
    if (![message isKindOfClass:[OTRBaseMessage class]]) {
830
        return trusted;
831
    }
832
    
833
    OTRBaseMessage *baseMessage = (OTRBaseMessage *)message;
834
    
835
    
836
    if (baseMessage.messageSecurityInfo.messageSecurity == OTRMessageTransportSecurityOTR) {
837
        NSData *otrFingerprintData = baseMessage.messageSecurityInfo.otrFingerprint;
838
        if ([otrFingerprintData length]) {
839
            trusted = [[[OTRProtocolManager sharedInstance].encryptionManager otrFingerprintForKey:self.threadKey collection:self.threadCollection fingerprint:otrFingerprintData] isTrusted];
840
        }
841
    } else if (baseMessage.messageSecurityInfo.messageSecurity == OTRMessageTransportSecurityOMEMO) {
842
        NSString *omemoDeviceYapKey = baseMessage.messageSecurityInfo.omemoDeviceYapKey;
843
        NSString *omemoDeviceYapCollection = baseMessage.messageSecurityInfo.omemoDeviceYapCollection;
844
        __block OTROMEMODevice *device = nil;
845
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
846
            device = [transaction objectForKey:omemoDeviceYapKey inCollection:omemoDeviceYapCollection];
847
        }];
848
        if(device != nil) {
849
            trusted = [device isTrusted];
850
        }
851
    }
852
    return trusted;
853
}
854

    
855
- (BOOL) isGroupChat {
856
    __block OTRXMPPRoom *room = nil;
857
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
858
        room = [self roomWithTransaction:transaction];
859
    }];
860
    return (room != nil);
861
}
862

    
863
#pragma - mark Profile Button Methods
864

    
865
- (void)setupInfoButton {
866
    if ([self isGroupChat]) {
867
        UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"112-group" inBundle:[OTRAssets resourcesBundle] compatibleWithTraitCollection:nil] style:UIBarButtonItemStylePlain target:self action:@selector(didSelectOccupantsButton:)];
868
        self.navigationItem.rightBarButtonItem = barButtonItem;
869
    } else {
870
        UIButton* infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
871
        infoButton.accessibilityIdentifier = @"profileButton";
872
        [infoButton addTarget:self action:@selector(infoButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
873
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:infoButton];
874
    }
875
}
876

    
877
- (void) infoButtonPressed:(id)sender {
878
    __block OTRAccount *account = nil;
879
    __block OTRBuddy *buddy = nil;
880
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
881
        account = [self accountWithTransaction:transaction];
882
        buddy = [self buddyWithTransaction:transaction];
883
    }];
884
    if (!account || !buddy) {
885
        return;
886
    }
887
    
888
    // Hack to manually re-fetch OMEMO devicelist because PEP sucks
889
    // TODO: Ideally this should be moved to some sort of manual refresh in the Profile view
890
    [self fetchOMEMODeviceList];
891
    
892
    XLFormDescriptor *form = [UserProfileViewController profileFormDescriptorForAccount:account buddies:@[buddy] connection:self.readOnlyDatabaseConnection];
893

    
894
    UserProfileViewController *verify = [[UserProfileViewController alloc] initWithAccountKey:account.uniqueId connection:self.readOnlyDatabaseConnection form:form];
895
    verify.completionBlock = ^{
896
        [self updateEncryptionState];
897
    };
898
    UINavigationController *verifyNav = [[UINavigationController alloc] initWithRootViewController:verify];
899
    verifyNav.modalPresentationStyle = UIModalPresentationFormSheet;
900
    [self presentViewController:verifyNav animated:YES completion:nil];
901
}
902

    
903
- (void)didSelectOccupantsButton:(id)sender {
904
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"OTRRoomOccupants" bundle:[OTRAssets resourcesBundle]];
905
    OTRRoomOccupantsViewController *occupantsVC = [storyboard instantiateViewControllerWithIdentifier:@"roomOccupants"];
906
    occupantsVC.delegate = self;
907
    [occupantsVC setupViewHandlerWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection roomKey:self.threadKey];
908
    [self.navigationController pushViewController:occupantsVC animated:YES];
909
}
910

    
911
// Hack to manually re-fetch OMEMO devicelist because PEP sucks
912
// TODO: Ideally this should be moved to some sort of manual refresh in the Profile view
913
-(void) fetchOMEMODeviceList {
914
    __block OTRAccount *account = nil;
915
    __block OTRBuddy *buddy = nil;
916
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
917
        account = [self accountWithTransaction:transaction];
918
        buddy = [self buddyWithTransaction:transaction];
919
    }];
920
    if (!account || !buddy) {
921
        return;
922
    }
923
    id manager = [[OTRProtocolManager sharedInstance] protocolForAccount:account];
924
    if ([manager isKindOfClass:[OTRXMPPManager class]]) {
925
        XMPPJID *jid = [XMPPJID jidWithString:buddy.username];
926
        OTRXMPPManager *xmpp = manager;
927
        [xmpp.omemoSignalCoordinator.omemoModule fetchDeviceIdsForJID:jid elementId:nil];
928
    }
929
}
930

    
931
- (UIBarButtonItem *)rightBarButtonItem
932
{
933
    if (!self.lockBarButtonItem) {
934
        self.lockBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.lockButton];
935
    }
936
    return self.lockBarButtonItem;
937
}
938

    
939
-(void)updateEncryptionState
940
{
941
    if ([self isGroupChat]) {
942
        __block OTRXMPPManager *xmpp = nil;
943
        [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
944
            xmpp = [self xmppManagerWithTransaction:transaction];
945
        } completionBlock:^{
946
            BOOL canSendMedia = NO;
947
            // Check for XEP-0363 HTTP upload
948
            // TODO: move this check elsewhere so it isnt dependent on refreshing crypto state
949
            if (xmpp != nil && xmpp.fileTransferManager.canUploadFiles) {
950
                canSendMedia = YES;
951
            }
952
            self.state.canSendMedia = canSendMedia;
953
            self.state.messageSecurity = OTRMessageTransportSecurityPlaintext;
954
            [self didUpdateState];
955
        }];
956
    } else {
957
        __block OTRBuddy *buddy = nil;
958
        __block OTRAccount *account = nil;
959
        __block OTRXMPPManager *xmpp = nil;
960
        __block OTRMessageTransportSecurity messageSecurity = OTRMessageTransportSecurityInvalid;
961
        
962
        [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
963
            buddy = [self buddyWithTransaction:transaction];
964
            account = [buddy accountWithTransaction:transaction];
965
            xmpp = [self xmppManagerWithTransaction:transaction];
966
            messageSecurity = [buddy preferredTransportSecurityWithTransaction:transaction];
967
        } completionBlock:^{
968
            BOOL canSendMedia = NO;
969
            // Check for XEP-0363 HTTP upload
970
            // TODO: move this check elsewhere so it isnt dependent on refreshing crypto state
971
            if (xmpp != nil && xmpp.fileTransferManager.canUploadFiles) {
972
                canSendMedia = YES;
973
            }
974
            if (!buddy || !account || !xmpp || (messageSecurity == OTRMessageTransportSecurityInvalid)) {
975
                DDLogError(@"updateEncryptionState error: missing parameters");
976
            } else {
977
                OTRKitMessageState messageState = [[OTRProtocolManager sharedInstance].encryptionManager.otrKit messageStateForUsername:buddy.username accountName:account.username protocol:account.protocolTypeString];
978
                if (messageState == OTRKitMessageStateEncrypted &&
979
                    buddy.status != OTRThreadStatusOffline) {
980
                    // If other side supports OTR, assume OTRDATA is possible
981
                    canSendMedia = YES;
982
                }
983
            }
984
            self.state.canSendMedia = canSendMedia;
985
            self.state.messageSecurity = messageSecurity;
986
            [self didUpdateState];
987
        }];
988
    }
989
}
990

    
991
- (void)setupAccessoryButtonsWithMessageState:(OTRKitMessageState)messageState buddyStatus:(OTRThreadStatus)status textViewHasText:(BOOL)hasText
992
{
993
    self.inputToolbar.contentView.rightBarButtonItem = self.sendButton;
994
    self.inputToolbar.sendButtonLocation = JSQMessagesInputSendButtonLocationRight;
995
    self.inputToolbar.contentView.leftBarButtonItem = nil;
996
}
997

    
998
- (void)connectButtonPressed:(id)sender
999
{
1000
    [self hideDropdownAnimated:YES completion:nil];
1001
    __block OTRAccount *account = nil;
1002
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1003
        account = [self accountWithTransaction:transaction];
1004
    }];
1005
    
1006
    if (account == nil) {
1007
        return;
1008
    }
1009
    
1010
    //If we have the password then we can login with that password otherwise show login UI to enter password
1011
    if ([account.password length]) {
1012
        [[OTRProtocolManager sharedInstance] loginAccount:account userInitiated:YES];
1013
        
1014
    } else {
1015
        OTRBaseLoginViewController *loginViewController = [[OTRBaseLoginViewController alloc] initWithAccount:account];
1016
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:loginViewController];
1017
        nav.modalPresentationStyle = UIModalPresentationFormSheet;
1018
        [self presentViewController:nav animated:YES completion:nil];
1019
    }
1020
    
1021
    
1022
}
1023

    
1024
#pragma - mark  dropDown Methods
1025

    
1026
- (void)showDropdownWithTitle:(NSString *)title buttons:(NSArray *)buttons animated:(BOOL)animated tag:(NSInteger)tag
1027
{
1028
    NSTimeInterval duration = 0.3;
1029
    if (!animated) {
1030
        duration = 0.0;
1031
    }
1032
    
1033
    self.buttonDropdownView = [[OTRButtonView alloc] initWithTitle:title buttons:buttons];
1034
    self.buttonDropdownView.tag = tag;
1035
    
1036
    CGFloat height = [OTRButtonView heightForTitle:title width:self.view.bounds.size.width buttons:buttons];
1037
    
1038
    [self.view addSubview:self.buttonDropdownView];
1039
    
1040
    [self.buttonDropdownView autoSetDimension:ALDimensionHeight toSize:height];
1041
    [self.buttonDropdownView autoPinEdgeToSuperviewEdge:ALEdgeLeading];
1042
    [self.buttonDropdownView autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
1043
    self.buttonDropdownView.topLayoutConstraint = [self.buttonDropdownView autoPinToTopLayoutGuideOfViewController:self withInset:height*-1];
1044
    
1045
    [self.buttonDropdownView layoutIfNeeded];
1046
    
1047
    [UIView animateWithDuration:duration animations:^{
1048
        self.buttonDropdownView.topLayoutConstraint.constant = 0.0;
1049
        [self.buttonDropdownView layoutIfNeeded];
1050
    } completion:nil];
1051
    
1052
}
1053

    
1054
- (void)hideDropdownAnimated:(BOOL)animated completion:(void (^)(void))completion
1055
{
1056
    if (!self.buttonDropdownView) {
1057
        if (completion) {
1058
            completion();
1059
        }
1060
    }
1061
    else {
1062
        NSTimeInterval duration = 0.3;
1063
        if (!animated) {
1064
            duration = 0.0;
1065
        }
1066
        
1067
        [UIView animateWithDuration:duration animations:^{
1068
            CGFloat height = self.buttonDropdownView.frame.size.height;
1069
            self.buttonDropdownView.topLayoutConstraint.constant = height*-1;
1070
            [self.buttonDropdownView layoutIfNeeded];
1071
            
1072
        } completion:^(BOOL finished) {
1073
            if (finished) {
1074
                [self.buttonDropdownView removeFromSuperview];
1075
                self.buttonDropdownView = nil;
1076
            }
1077
            
1078
            if (completion) {
1079
                completion();
1080
            }
1081
        }];
1082
    }
1083
}
1084

    
1085
- (void)saveCurrentMessageText:(NSString *)text threadKey:(NSString *)key colleciton:(NSString *)collection
1086
{
1087
    if (![key length] || ![collection length]) {
1088
        return;
1089
    }
1090
    
1091
    [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
1092
        id <OTRThreadOwner> thread = [[transaction objectForKey:key inCollection:collection] copy];
1093
        if (thread == nil) {
1094
            // this can happen when we've just approved a contact, then the thread key
1095
            // might have changed.
1096
            return;
1097
        }
1098
        [thread setCurrentMessageText:text];
1099
        [transaction setObject:thread forKey:key inCollection:collection];
1100
        
1101
        //Send inactive chat State
1102
        OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction];
1103
        OTRXMPPManager *xmppManager = (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
1104
        if (![text length]) {
1105
            [xmppManager sendChatState:OTRChatStateInactive withBuddyID:[thread threadIdentifier]];
1106
        }
1107
    }];
1108
}
1109

    
1110
//* Takes the current value out of the thread object and sets it to the text view and nils out result*/
1111
- (void)moveLastComposingTextForThreadKey:(NSString *)key colleciton:(NSString *)collection toTextView:(UITextView *)textView {
1112
    if (![key length] || ![collection length] || !textView) {
1113
        return;
1114
    }
1115
    __block id <OTRThreadOwner> thread = nil;
1116
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1117
        thread = [[transaction objectForKey:key inCollection:collection] copy];
1118
    }];
1119
    // Don't remove text you're already composing
1120
    NSString *oldThreadText = [thread currentMessageText];
1121
    if (!textView.text.length && oldThreadText.length) {
1122
        textView.text = oldThreadText;
1123
        [self receivedTextViewChanged:textView];
1124
    }
1125
    if (oldThreadText.length) {
1126
        [thread setCurrentMessageText:nil];
1127
        [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
1128
            [transaction setObject:thread forKey:key inCollection:collection];
1129
        }];
1130
    }
1131
}
1132

    
1133
- (id <OTRMessageProtocol,JSQMessageData>)messageAtIndexPath:(NSIndexPath *)indexPath
1134
{
1135
    // Multiple invocations with the same indexPath tend to come in groups, no need to hit the DB each time.
1136
    // Even though the object is cached, the row ID calculation still takes time
1137
    if (![indexPath isEqual:self.currentIndexPath]) {
1138
        self.currentIndexPath = indexPath;
1139
        self.currentMessage = [self.viewHandler object:indexPath];
1140
    }
1141
    return self.currentMessage;
1142
}
1143

    
1144
/**
1145
 * Updates the flexible range of the DB connection.
1146
 * @param reset When NO, adds kOTRMessagePageSize to the range length, when YES resets the length to the kOTRMessagePageSize
1147
 */
1148
- (void)updateRangeOptions:(BOOL)reset
1149
{
1150
    YapDatabaseViewRangeOptions *options = [self.viewHandler.mappings rangeOptionsForGroup:self.threadKey];
1151
    if (reset) {
1152
        if (options != nil && !self.messageRangeExtended) {
1153
            return;
1154
        }
1155
        options = [YapDatabaseViewRangeOptions flexibleRangeWithLength:kOTRMessagePageSize
1156
                                                                offset:0
1157
                                                                  from:YapDatabaseViewEnd];
1158
        self.messageSizeCache.countLimit = kOTRMessagePageSize;
1159
        self.messageRangeExtended = NO;
1160
    } else {
1161
        options = [options copyWithNewLength:options.length + kOTRMessagePageSize];
1162
        self.messageSizeCache.countLimit += kOTRMessagePageSize;
1163
        self.messageRangeExtended = YES;
1164
    }
1165
    [self.viewHandler.mappings setRangeOptions:options forGroup:self.threadKey];
1166
    
1167
    self.loadingMessages = YES;
1168
    
1169
    CGFloat distanceToBottom = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
1170
    
1171
    [self.collectionView reloadData];
1172
    
1173
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1174
        NSUInteger shownCount = [self.viewHandler.mappings numberOfItemsInGroup:self.threadKey];
1175
        NSUInteger totalCount = [[transaction ext:OTRFilteredChatDatabaseViewExtensionName] numberOfItemsInGroup:self.threadKey];
1176
        [self setShowLoadEarlierMessagesHeader:shownCount < totalCount];
1177
    }];
1178
    
1179
    if (!reset) {
1180
        [self.collectionView.collectionViewLayout invalidateLayout];
1181
        [self.collectionView layoutSubviews];
1182
        self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - distanceToBottom);
1183
    }
1184
    
1185
    self.loadingMessages = NO;
1186
}
1187

    
1188
- (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath
1189
{
1190
    BOOL showDate = NO;
1191
    if (indexPath.row == 0) {
1192
        showDate = YES;
1193
    }
1194
    else {
1195
        id <OTRMessageProtocol> currentMessage = [self messageAtIndexPath:indexPath];
1196
        id <OTRMessageProtocol> previousMessage = [self messageAtIndexPath:[NSIndexPath indexPathForItem:indexPath.row-1 inSection:indexPath.section]];
1197
        
1198
        NSTimeInterval timeDifference = [[currentMessage messageDate] timeIntervalSinceDate:[previousMessage messageDate]];
1199
        if (timeDifference > kOTRMessageSentDateShowTimeInterval) {
1200
            showDate = YES;
1201
        }
1202
    }
1203
    return showDate;
1204
}
1205

    
1206
- (BOOL)showSenderDisplayNameAtIndexPath:(NSIndexPath *)indexPath {
1207
    id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1208
    
1209
    if(![self.threadCollection isEqualToString:[OTRXMPPRoom collection]]) {
1210
        return NO;
1211
    }
1212
    
1213
    if ([[message senderId] isEqualToString:self.senderId]) {
1214
        return NO;
1215
    }
1216
    
1217
    if(indexPath.row -1 >= 0) {
1218
        NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section];
1219
        id<OTRMessageProtocol,JSQMessageData> previousMessage = [self messageAtIndexPath:previousIndexPath];
1220
        if ([[previousMessage senderId] isEqualToString:message.senderId]) {
1221
            return NO;
1222
        }
1223
    }
1224
    
1225
    return YES;
1226
}
1227

    
1228
- (BOOL)isPushMessageAtIndexPath:(NSIndexPath *)indexPath {
1229
    id message = [self messageAtIndexPath:indexPath];
1230
    return [message isKindOfClass:[PushMessage class]];
1231
}
1232

    
1233
- (void)receivedTextViewChangedNotification:(NSNotification *)notification
1234
{
1235
    //Check if the text state changes from having some text to some or vice versa
1236
    UITextView *textView = notification.object;
1237
    [self receivedTextViewChanged:textView];
1238
}
1239

    
1240
- (void)receivedTextViewChanged:(UITextView *)textView {
1241
    BOOL hasText = [textView.text length] > 0;
1242
    if(hasText != self.state.hasText) {
1243
        self.state.hasText = hasText;
1244
        [self didUpdateState];
1245
    }
1246
    
1247
    //Everytime the textview has text and a notification comes through we are 'typing' otherwise we are done typing
1248
    if (hasText) {
1249
        [self isTyping];
1250
    } else {
1251
        [self didFinishTyping];
1252
    }
1253
    
1254
    return;
1255

    
1256
}
1257

    
1258
#pragma - mark Update UI
1259

    
1260
- (void)didUpdateState {
1261
    
1262
}
1263

    
1264
- (void)isTyping {
1265
    
1266
}
1267

    
1268
- (void)didFinishTyping {
1269
    
1270
}
1271

    
1272
#pragma - mark Sending Media Items
1273

    
1274
- (void)sendMediaItem:(OTRMediaItem *)mediaItem data:(NSData *)data message:(id<OTRMessageProtocol>)message transaction:(YapDatabaseReadWriteTransaction *)transaction
1275
{
1276
    id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
1277
    OTRXMPPManager *xmpp = [self xmppManagerWithTransaction:transaction];
1278
    if (!message || !thread || !xmpp) {
1279
        DDLogError(@"Error sending file due to bad paramters");
1280
        return;
1281
    }
1282
    if (data) {
1283
        thread.lastMessageIdentifier = message.messageKey;
1284
        [thread saveWithTransaction:transaction];
1285
    }
1286
    // XEP-0363
1287
    [xmpp.fileTransferManager sendWithMediaItem:mediaItem prefetchedData:data message:message];
1288
    
1289
    [mediaItem touchParentMessageWithTransaction:transaction];
1290
}
1291

    
1292
#pragma - mark Media Display Methods
1293

    
1294
- (void)showImage:(OTRImageItem *)imageItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1295
{
1296
    //FIXME: Possible for image to not be in cache?
1297
    UIImage *image = [OTRImages imageWithIdentifier:imageItem.uniqueId];
1298
    JTSImageInfo *imageInfo = [[JTSImageInfo alloc] init];
1299
    imageInfo.image = image;
1300
    
1301
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
1302
    if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
1303
        UIView *cellContainterView = ((JSQMessagesCollectionViewCell *)cell).messageBubbleContainerView;
1304
        imageInfo.referenceRect = cellContainterView.bounds;
1305
        imageInfo.referenceView = cellContainterView;
1306
        imageInfo.referenceCornerRadius = 10;
1307
    }
1308
    
1309
    JTSImageViewController *imageViewer = [[JTSImageViewController alloc]
1310
                                           initWithImageInfo:imageInfo
1311
                                           mode:JTSImageViewControllerMode_Image
1312
                                           backgroundStyle:JTSImageViewControllerBackgroundOption_Blurred];
1313
    
1314
    [imageViewer showFromViewController:self transition:JTSImageViewControllerTransition_FromOriginalPosition];
1315
}
1316

    
1317
- (void)showVideo:(OTRVideoItem *)videoItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1318
{
1319
    if (videoItem.filename) {
1320
        NSURL *videoURL = [[OTRMediaServer sharedInstance] urlForMediaItem:videoItem buddyUniqueId:self.threadKey];
1321
        MPMoviePlayerViewController *moviePlayerViewController = [[MPMoviePlayerViewController alloc] initWithContentURL:videoURL];
1322
        [self presentViewController:moviePlayerViewController animated:YES completion:nil];
1323
    }
1324
}
1325

    
1326
- (void)playOrPauseAudio:(OTRAudioItem *)audioItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1327
{
1328
    NSError *error = nil;
1329
    if  ([audioItem.uniqueId isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
1330
        if  ([self.audioPlaybackController isPlaying]) {
1331
            [self.audioPlaybackController pauseCurrentlyPlaying];
1332
        }
1333
        else {
1334
            [self.audioPlaybackController resumeCurrentlyPlaying];
1335
        }
1336
    }
1337
    else {
1338
        [self.audioPlaybackController stopCurrentlyPlaying];
1339
        OTRAudioControlsView *audioControls = [self audioControllsfromCollectionView:collectionView atIndexPath:indexPath];
1340
        [self.audioPlaybackController attachAudioControlsView:audioControls];
1341
        [self.audioPlaybackController playAudioItem:audioItem buddyUniqueId:self.threadKey error:&error];
1342
    }
1343
    
1344
    if (error) {
1345
         DDLogError(@"Audio Playback Error: %@",error);
1346
    }
1347
   
1348
}
1349

    
1350
- (OTRAudioControlsView *)audioControllsfromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath {
1351
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
1352
    if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
1353
        UIView *mediaView = ((JSQMessagesCollectionViewCell *)cell).mediaView;
1354
        UIView *view = [mediaView viewWithTag:kOTRAudioControlsViewTag];
1355
        if ([view isKindOfClass:[OTRAudioControlsView class]]) {
1356
            return (OTRAudioControlsView *)view;
1357
        }
1358
    }
1359
    
1360
    return nil;
1361
}
1362

    
1363
#pragma MARK - OTRMessagesCollectionViewFlowLayoutSizeProtocol methods
1364

    
1365
- (BOOL)hasBubbleSizeForCellAtIndexPath:(NSIndexPath *)indexPath {
1366
    return ![self isPushMessageAtIndexPath:indexPath];
1367
}
1368

    
1369
#pragma mark - JSQMessagesViewController method overrides
1370

    
1371
- (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
1372
{
1373
    JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath];
1374
    
1375
    //Fixes times when there needs to be two lines (date & knock sent) and doesn't seem to affect one line instances
1376
    cell.cellTopLabel.numberOfLines = 0;
1377
    
1378
    id <OTRMessageProtocol>message = [self messageAtIndexPath:indexPath];
1379
    
1380
    __block OTRXMPPAccount *account = nil;
1381
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1382
        account = (OTRXMPPAccount*)[self accountWithTransaction:transaction];
1383
    }];
1384
    
1385
    UIColor *textColor = nil;
1386
    if ([message isMessageIncoming]) {
1387
        textColor = [UIColor blackColor];
1388
    }
1389
    else {
1390
        textColor = [UIColor whiteColor];
1391
    }
1392
    if (cell.textView != nil)
1393
        cell.textView.textColor = textColor;
1394

    
1395
	// Do not allow clickable links for Tor accounts to prevent information leakage
1396
    // Could be better to move this information to the message object to not need to do a database read.
1397
    if ([account isKindOfClass:[OTRXMPPTorAccount class]]) {
1398
        cell.textView.dataDetectorTypes = UIDataDetectorTypeNone;
1399
    }
1400
    else {
1401
        cell.textView.dataDetectorTypes = UIDataDetectorTypeLink;
1402
        cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : textColor,
1403
                                              NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) };
1404
    }
1405
    
1406
    if ([[message messageMediaItemKey] isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
1407
        UIView *view = [cell.mediaView viewWithTag:kOTRAudioControlsViewTag];
1408
        if ([view isKindOfClass:[OTRAudioControlsView class]]) {
1409
            [self.audioPlaybackController attachAudioControlsView:(OTRAudioControlsView *)view];
1410
        }
1411
    }
1412
    
1413
    // Needed for link interaction
1414
    cell.textView.delegate = self;
1415
    return cell;
1416
}
1417

    
1418
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
1419
{
1420
    if (action == @selector(delete:)) {
1421
        return YES;
1422
    }
1423
    
1424
    return [super collectionView:collectionView canPerformAction:action forItemAtIndexPath:indexPath withSender:sender];
1425
}
1426

    
1427
- (void)didPressSendButton:(UIButton *)button withMessageText:(NSString *)text senderId:(NSString *)senderId senderDisplayName:(NSString *)senderDisplayName date:(NSDate *)date
1428
{
1429
    if(!text.length) {
1430
        return;
1431
    }
1432
    
1433
    self.navigationController.providesPresentationContextTransitionStyle = YES;
1434
    self.navigationController.definesPresentationContext = YES;
1435
    
1436
    //0. Clear out message text immediately
1437
    //   This is to prevent the scenario where multiple messages get sent because the message text isn't cleared out
1438
    //   due to aggregated touch events during UI pauses.
1439
    //   A side effect is that sent messages may not appear in the UI immediately
1440
    [self finishSendingMessage];
1441
    
1442
    __block id<OTRMessageProtocol> message = nil;
1443
    __block OTRXMPPManager *xmpp = nil;
1444
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1445
        id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
1446
        message = [thread outgoingMessageWithText:text transaction:transaction];
1447
        xmpp = [self xmppManagerWithTransaction:transaction];
1448
    }];
1449
    if (!message || !xmpp) { return; }
1450
    [xmpp enqueueMessage:message];
1451
}
1452

    
1453
- (void)didPressAccessoryButton:(UIButton *)sender
1454
{
1455
    if ([sender isEqual:self.cameraButton]) {
1456
        [self.attachmentPicker showAlertControllerFromSourceView:sender withCompletion:nil];
1457
    }
1458
}
1459

    
1460
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
1461
{
1462
    if (action == @selector(delete:)) {
1463
        [self deleteMessageAtIndexPath:indexPath];
1464
    }
1465
    else {
1466
        [super collectionView:collectionView performAction:action forItemAtIndexPath:indexPath withSender:sender];
1467
    }
1468
}
1469

    
1470
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
1471
{
1472
    id <OTRMessageProtocol, JSQMessageData> message = [self messageAtIndexPath:indexPath];
1473

    
1474
    NSNumber *key = @(message.messageHash);
1475
    NSValue *sizeValue = [self.messageSizeCache objectForKey:key];
1476
    if (sizeValue != nil) {
1477
        return [sizeValue CGSizeValue];
1478
    }
1479

    
1480
    // Although JSQMessagesBubblesSizeCalculator has its own cache, its size is fixed and quite small, so it quickly chokes on scrolling into the past
1481
    CGSize size = [super collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath];
1482
    // The height of the first cell might change: on loading additional messages the date label most likely will disappear
1483
    if (indexPath.row > 0) {
1484
        [self.messageSizeCache setObject:[NSValue valueWithCGSize:size] forKey:key];
1485
    }
1486
    return size;
1487
}
1488

    
1489
#pragma - mark UIPopoverPresentationControllerDelegate Methods
1490

    
1491
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController {
1492
    // Without setting this, there will be a crash on iPad
1493
    // This delegate is set in the OTRAttachmentPicker
1494
    popoverPresentationController.sourceView = self.cameraButton;
1495
}
1496

    
1497
- (void)sendPhoto:(UIImage *)photo asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize {
1498
    NSParameterAssert(photo);
1499
    if (!photo) { return; }
1500
    __block OTRXMPPManager *xmpp = nil;
1501
    __block id<OTRThreadOwner> thread = nil;
1502
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1503
        xmpp = [self xmppManagerWithTransaction:transaction];
1504
        thread = [self threadObjectWithTransaction:transaction];
1505
    }];
1506
    NSParameterAssert(xmpp);
1507
    NSParameterAssert(thread);
1508
    if (!xmpp || !thread) { return; }
1509

    
1510
    [xmpp.fileTransferManager sendWithImage:photo thread:thread];
1511
}
1512

    
1513
#pragma - mark OTRAttachmentPickerDelegate Methods
1514

    
1515
- (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotPhoto:(UIImage *)photo withInfo:(NSDictionary *)info
1516
{
1517
    [self sendPhoto:photo asJPEG:YES shouldResize:YES];
1518
}
1519

    
1520
- (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotVideoURL:(NSURL *)videoURL
1521
{
1522
    if (!videoURL) { return; }
1523
    __block OTRXMPPManager *xmpp = nil;
1524
    __block id<OTRThreadOwner> thread = nil;
1525
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1526
        xmpp = [self xmppManagerWithTransaction:transaction];
1527
        thread = [self threadObjectWithTransaction:transaction];
1528
    }];
1529
    NSParameterAssert(xmpp);
1530
    NSParameterAssert(thread);
1531
    if (!xmpp || !thread) { return; }
1532

    
1533
    [xmpp.fileTransferManager sendWithVideoURL:videoURL thread:thread];
1534
}
1535

    
1536
- (NSArray <NSString *>*)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker preferredMediaTypesForSource:(UIImagePickerControllerSourceType)source
1537
{
1538
    return @[(NSString*)kUTTypeImage];
1539
}
1540

    
1541
- (void)sendAudioFileURL:(NSURL *)url
1542
{
1543
    if (!url) { return; }
1544
    __block OTRXMPPManager *xmpp = nil;
1545
    __block id<OTRThreadOwner> thread = nil;
1546
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1547
        xmpp = [self xmppManagerWithTransaction:transaction];
1548
        thread = [self threadObjectWithTransaction:transaction];
1549
    }];
1550
    NSParameterAssert(xmpp);
1551
    NSParameterAssert(thread);
1552
    if (!xmpp || !thread) { return; }
1553
    
1554
    [xmpp.fileTransferManager sendWithAudioURL:url thread:thread];
1555
}
1556

    
1557
- (void)sendImageFilePath:(NSString *)filePath asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize
1558
{
1559
    [self sendPhoto:[UIImage imageWithContentsOfFile:filePath] asJPEG:asJPEG shouldResize:shouldResize];
1560
}
1561

    
1562

    
1563
#pragma - mark UIScrollViewDelegate Methods
1564

    
1565
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
1566
{
1567
    [self hideDropdownAnimated:YES completion:nil];
1568
}
1569

    
1570
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
1571
{
1572
    if (!self.loadingMessages) {
1573
        UIEdgeInsets insets = scrollView.contentInset;
1574
        CGFloat highestOffset = -insets.top;
1575
        CGFloat lowestOffset = scrollView.contentSize.height - scrollView.frame.size.height + insets.bottom;
1576
        CGFloat pos = scrollView.contentOffset.y;
1577

    
1578
        if (self.showLoadEarlierMessagesHeader && (pos == highestOffset || (pos < 0 && (scrollView.isDecelerating || scrollView.isDragging)))) {
1579
            [self updateRangeOptions:NO];
1580
        } else if (pos == lowestOffset) {
1581
            [self updateRangeOptions:YES];
1582
        }
1583
    }
1584
}
1585

    
1586
#pragma mark - UICollectionView DataSource
1587
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
1588
{
1589
    NSInteger numberOfMessages = [self.viewHandler.mappings numberOfItemsInSection:section];
1590
    return numberOfMessages;
1591
}
1592

    
1593
#pragma - mark JSQMessagesCollectionViewDataSource Methods
1594

    
1595
- (NSString *)senderDisplayName
1596
{
1597
    __block OTRAccount *account = nil;
1598
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1599
        account = [self accountWithTransaction:transaction];
1600
    }];
1601
    
1602
    NSString *senderDisplayName = @"";
1603
    if (account) {
1604
        if ([account.displayName length]) {
1605
            senderDisplayName = account.displayName;
1606
        } else {
1607
            senderDisplayName = account.username;
1608
        }
1609
    }
1610
    
1611
    return senderDisplayName;
1612
}
1613

    
1614
- (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1615
{
1616
    return (id <JSQMessageData>)[self messageAtIndexPath:indexPath];
1617
}
1618

    
1619
- (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1620
{
1621
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1622
    JSQMessagesBubbleImage *image = nil;
1623
    if ([message isMessageIncoming]) {
1624
        image = self.incomingBubbleImage;
1625
    }
1626
    else {
1627
        image = self.outgoingBubbleImage;
1628
    }
1629
    return image;
1630
}
1631

    
1632
- (id <JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1633
{
1634
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1635
    if ([message isKindOfClass:[PushMessage class]]) {
1636
        return nil;
1637
    }
1638
    
1639
    NSError *messageError = [message messageError];
1640
    if ((messageError && !messageError.isAutomaticDownloadError) ||
1641
        ![self isMessageTrusted:message]) {
1642
        return [self warningAvatarImage];
1643
    }
1644
    
1645
    if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
1646
        OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message;
1647
        __block OTRXMPPRoomOccupant *roomOccupant = nil;
1648
        __block OTRXMPPBuddy *roomOccupantBuddy = nil;
1649
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1650
            roomOccupant = [OTRXMPPRoomOccupant occupantWithJid:[XMPPJID jidWithString:roomMessage.senderJID] realJID:[XMPPJID jidWithString:roomMessage.senderJID] roomJID:[XMPPJID jidWithString:roomMessage.roomJID] accountId:[self accountWithTransaction:transaction].uniqueId createIfNeeded:NO transaction:transaction];
1651
            if (roomOccupant != nil) {
1652
                roomOccupantBuddy = [roomOccupant buddyWith:transaction];
1653
            }
1654
        }];
1655
        UIImage *avatarImage = nil;
1656
        if (roomOccupant) {
1657
            if (roomOccupantBuddy != nil) {
1658
                avatarImage = [roomOccupantBuddy avatarImage];
1659
            }
1660
            if (!avatarImage) {
1661
                avatarImage = [roomOccupant avatarImage];
1662
            }
1663
        } else {
1664
            avatarImage = [OTRImages avatarImageWithUsername:[[XMPPJID jidWithString:roomMessage.senderJID] resource]];
1665
        }
1666
        if (avatarImage) {
1667
            NSUInteger diameter = MIN(avatarImage.size.width, avatarImage.size.height);
1668
            return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1669
        }
1670
    }
1671
    
1672
    if ([message isMessageIncoming]) {
1673
        return [self buddyAvatarImage];
1674
    }
1675

    
1676
    return [self accountAvatarImage];
1677
}
1678

    
1679
- (JSQMessagesAvatarImage *)createAvatarImage:(UIImage *(^)(YapDatabaseReadTransaction *))getImage
1680
{
1681
    __block UIImage *avatarImage;
1682
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1683
        avatarImage = getImage(transaction);
1684
    }];
1685
    if (avatarImage != nil) {
1686
        NSUInteger diameter = (NSUInteger) MIN(avatarImage.size.width, avatarImage.size.height);
1687
        return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1688
    }
1689
    return nil;
1690
}
1691

    
1692
- (JSQMessagesAvatarImage *)warningAvatarImage
1693
{
1694
    if (_warningAvatarImage == nil) {
1695
        _warningAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1696
            return [OTRImages circleWarningWithColor:[OTRColors warnColor]];
1697
        }];
1698
    }
1699
    return _warningAvatarImage;
1700
}
1701

    
1702
- (JSQMessagesAvatarImage *)accountAvatarImage
1703
{
1704
    if (_accountAvatarImage == nil) {
1705
        _accountAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1706
            return [[self accountWithTransaction:transaction] avatarImage];
1707
        }];
1708
    }
1709
    return _accountAvatarImage;
1710
}
1711

    
1712
- (JSQMessagesAvatarImage *)buddyAvatarImage
1713
{
1714
    if (_buddyAvatarImage == nil) {
1715
        _buddyAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1716
            return [[self buddyWithTransaction:transaction] avatarImage];
1717
        }];
1718
    }
1719
    return _buddyAvatarImage;
1720
}
1721

    
1722
////// Optional //////
1723

    
1724
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
1725
{
1726
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init];
1727
    
1728
    if ([self showDateAtIndexPath:indexPath]) {
1729
        id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1730
        NSDate *date = [message messageDate];
1731
        if (date != nil) {
1732
            [text appendAttributedString: [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:date]];
1733
        }
1734
    }
1735
    
1736
    if ([self isPushMessageAtIndexPath:indexPath]) {
1737
        JSQMessagesTimestampFormatter *formatter = [JSQMessagesTimestampFormatter sharedFormatter];
1738
        NSString *knockString = KNOCK_SENT_STRING();
1739
        //Add new line if there is already a date string
1740
        if ([text length] > 0) {
1741
            knockString = [@"\n" stringByAppendingString:knockString];
1742
        }
1743
        [text appendAttributedString:[[NSAttributedString alloc] initWithString:knockString attributes:formatter.dateTextAttributes]];
1744
    }
1745
    
1746
    return text;
1747
}
1748

    
1749

    
1750
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1751
{
1752
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1753
        id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1754
        
1755
        __block NSString *displayName = nil;
1756
        if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
1757
            OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message;
1758
            [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1759
                OTRXMPPRoomOccupant *occupant = [OTRXMPPRoomOccupant occupantWithJid:[XMPPJID jidWithString:roomMessage.senderJID] realJID:[XMPPJID jidWithString:roomMessage.senderJID] roomJID:[XMPPJID jidWithString:roomMessage.roomJID] accountId:[self accountWithTransaction:transaction].uniqueId createIfNeeded:NO transaction:transaction];
1760
                if (occupant) {
1761
                    OTRXMPPBuddy *buddy = [occupant buddyWith:transaction];
1762
                    if (buddy) {
1763
                        displayName = [buddy displayName];
1764
                    } else {
1765
                        displayName = [[XMPPJID jidWithString:occupant.jid] resource];
1766
                    }
1767
                }
1768
            }];
1769
        }
1770
        if (!displayName) {
1771
            displayName = [message senderDisplayName];
1772
        }
1773
        return [[NSAttributedString alloc] initWithString:displayName];
1774
    }
1775
    
1776
    return  nil;
1777
}
1778

    
1779
/** Currently uses clock for queued, and checkmark for delivered. */
1780
- (nullable NSAttributedString*) deliveryStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
1781
    if (!message) { return nil; }
1782
    // Only applies to outgoing messages
1783
    if ([message isMessageIncoming]) {
1784
        return nil;
1785
    }
1786
    NSString *deliveryStatusString = nil;
1787
    if(message.isMessageSent == NO && ![message messageMediaItemKey]) {
1788
        // Waiting to send message. This message is in the queue.
1789
        deliveryStatusString = [NSString fa_stringForFontAwesomeIcon:FAClockO];
1790
    } else if (message.isMessageDelivered){
1791
        deliveryStatusString = [NSString stringWithFormat:@"%@ ",[NSString fa_stringForFontAwesomeIcon:FACheck]];
1792
    }
1793
    if (deliveryStatusString != nil) {
1794
        UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1795
        if (!font) {
1796
            font = [UIFont systemFontOfSize:12];
1797
        }
1798
        return [[NSAttributedString alloc] initWithString:deliveryStatusString attributes:@{NSFontAttributeName: font}];
1799
    }
1800
    return nil;
1801
}
1802

    
1803
- (nullable NSAttributedString *) encryptionStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
1804
    NSString *lockString = nil;
1805
    if (message.messageSecurity == OTRMessageTransportSecurityOTR) {
1806
        lockString = [NSString stringWithFormat:@"%@ OTR ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1807
    } else if (message.messageSecurity == OTRMessageTransportSecurityOMEMO) {
1808
        lockString = [NSString stringWithFormat:@"%@ OMEMO ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1809
    }
1810
    else {
1811
        lockString = [NSString fa_stringForFontAwesomeIcon:FAUnlock];
1812
    }
1813
    UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1814
    if (!font) {
1815
        font = [UIFont systemFontOfSize:12];
1816
    }
1817
    return [[NSAttributedString alloc] initWithString:lockString attributes:@{NSFontAttributeName: font}];
1818
}
1819

    
1820

    
1821
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
1822
{
1823
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1824
    if (!message) {
1825
        return [[NSAttributedString alloc] initWithString:@""];
1826
    }
1827
    
1828
    UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1829
    if (!font) {
1830
        font = [UIFont systemFontOfSize:12];
1831
    }
1832
    NSDictionary *iconAttributes = @{NSFontAttributeName: font};
1833
    NSDictionary *lockAttributes = [iconAttributes copy];
1834
    
1835
    ////// Lock Icon //////
1836
    NSAttributedString *lockString = [self encryptionStatusStringForMessage:message];
1837
    if (!lockString) {
1838
        lockString = [[NSAttributedString alloc] initWithString:@""];
1839
    }
1840
    NSMutableAttributedString *attributedString = [lockString mutableCopy];
1841
    
1842
    BOOL trusted = YES;
1843
    if([message isKindOfClass:[OTRBaseMessage class]]) {
1844
        trusted = [self isMessageTrusted:message];
1845
    };
1846
    
1847
    if (!trusted) {
1848
        NSMutableDictionary *mutableCopy = [lockAttributes mutableCopy];
1849
        [mutableCopy setObject:[UIColor redColor] forKey:NSForegroundColorAttributeName];
1850
        lockAttributes = mutableCopy;
1851
    }
1852
    
1853
    NSAttributedString *deliveryString = [self deliveryStatusStringForMessage:message];
1854
    if (deliveryString) {
1855
        [attributedString appendAttributedString:deliveryString];
1856
    }
1857
    
1858
    if([[message messageMediaItemKey] length] > 0) {
1859
        
1860
        __block OTRMediaItem *mediaItem = nil;
1861
        //Get the media item
1862
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
1863
            mediaItem = [OTRMediaItem fetchObjectWithUniqueID:[message messageMediaItemKey] transaction:transaction];
1864
        }];
1865
        if (!mediaItem) {
1866
            return attributedString;
1867
        }
1868
        
1869
        float percentProgress = mediaItem.transferProgress * 100;
1870
        
1871
        NSString *progressString = nil;
1872
        NSUInteger insertIndex = 0;
1873
        
1874
        if (mediaItem.isIncoming && mediaItem.transferProgress < 1) {
1875
            if (message.messageError) {
1876
                progressString = [NSString stringWithFormat:@"%@ ",WAITING_STRING()];
1877
            } else {
1878
                progressString = [NSString stringWithFormat:@" %@ %.0f%%",INCOMING_STRING(),percentProgress];
1879
            }
1880
            insertIndex = [attributedString length];
1881
        } else if (!mediaItem.isIncoming && mediaItem.transferProgress < 1) {
1882
            if(percentProgress > 0) {
1883
                progressString = [NSString stringWithFormat:@"%@ %.0f%% ",SENDING_STRING(),percentProgress];
1884
            } else {
1885
                progressString = [NSString stringWithFormat:@"%@ ",WAITING_STRING()];
1886
            }
1887
        }
1888
        
1889
        if ([progressString length]) {
1890
            UIFont *font = [UIFont systemFontOfSize:12];
1891
            [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:progressString attributes:@{NSFontAttributeName: font}] atIndex:insertIndex];
1892
        }
1893
    }
1894
    
1895
    return attributedString;
1896
}
1897

    
1898

    
1899
#pragma - mark  JSQMessagesCollectionViewDelegateFlowLayout Methods
1900

    
1901
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1902
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1903
heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
1904
{
1905
    CGFloat height = 0.0f;
1906
    if ([self showDateAtIndexPath:indexPath]) {
1907
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1908
    }
1909
    
1910
    if ([self isPushMessageAtIndexPath:indexPath]) {
1911
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1912
    }
1913
    return height;
1914
}
1915

    
1916
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1917
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1918
heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1919
{
1920
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1921
        return kJSQMessagesCollectionViewCellLabelHeightDefault;
1922
    }
1923
    return 0.0f;
1924
}
1925

    
1926
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1927
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1928
heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
1929
{
1930
    CGFloat height = kJSQMessagesCollectionViewCellLabelHeightDefault;
1931
    if ([self isPushMessageAtIndexPath:indexPath]) {
1932
        height = 0.0f;
1933
    }
1934
    return height;
1935
}
1936

    
1937
- (void)deleteMessageAtIndexPath:(NSIndexPath *)indexPath
1938
{
1939
    __block id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1940
    __weak __typeof__(self) weakSelf = self;
1941
    [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
1942
        __typeof__(self) strongSelf = weakSelf;
1943
        [transaction removeObjectForKey:[message messageKey] inCollection:[message messageCollection]];
1944
        //Update Last message date for sorting and grouping
1945
        OTRBuddy *buddy = [[strongSelf buddyWithTransaction:transaction] copy];
1946
        buddy.lastMessageId = nil;
1947
        [buddy saveWithTransaction:transaction];
1948
    }];
1949
}
1950

    
1951
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapAvatarImageView:(UIImageView *)avatarImageView atIndexPath:(NSIndexPath *)indexPath
1952
{
1953
    id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1954
    [self didTapAvatar:message sender:avatarImageView];
1955
}
1956

    
1957
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath
1958
{
1959
    id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1960
    if (!message.isMediaMessage) {
1961
        return;
1962
    }
1963
    __block OTRMediaItem *item = nil;
1964
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
1965
         item = [OTRMediaItem mediaItemForMessage:message transaction:transaction];
1966
    }];
1967
    if (!item) { return; }
1968
    if (item.transferProgress != 1 && item.isIncoming) {
1969
        return;
1970
    }
1971
    
1972
    if ([item isKindOfClass:[OTRImageItem class]]) {
1973
        [self showImage:(OTRImageItem *)item fromCollectionView:collectionView atIndexPath:indexPath];
1974
    }
1975
    else if ([item isKindOfClass:[OTRVideoItem class]]) {
1976
        [self showVideo:(OTRVideoItem *)item fromCollectionView:collectionView atIndexPath:indexPath];
1977
    }
1978
    else if ([item isKindOfClass:[OTRAudioItem class]]) {
1979
        [self playOrPauseAudio:(OTRAudioItem *)item fromCollectionView:collectionView atIndexPath:indexPath];
1980
    } else if ([message conformsToProtocol:@protocol(OTRDownloadMessage)]) {
1981
        id<OTRDownloadMessage> download = (id<OTRDownloadMessage>)message;
1982
        // Janky hack to open URL for now
1983
        NSArray<UIAlertAction*> *actions = [UIAlertAction actionsForMediaMessage:download sourceView:self.view viewController:self];
1984
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:message.text message:nil preferredStyle:UIAlertControllerStyleActionSheet];
1985
        [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
1986
            [alert addAction:obj];
1987
        }];
1988
        [alert addAction:[self cancleAction]];
1989
        
1990
        // Get the anchor
1991
        alert.popoverPresentationController.sourceView = self.view;
1992
        alert.popoverPresentationController.sourceRect = self.view.bounds;
1993
        UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
1994
        if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
1995
            UIView *cellContainterView = ((JSQMessagesCollectionViewCell *)cell).messageBubbleContainerView;
1996
            alert.popoverPresentationController.sourceRect = cellContainterView.bounds;
1997
            alert.popoverPresentationController.sourceView = cellContainterView;
1998
        }
1999

    
2000
        [self presentViewController:alert animated:YES completion:nil];
2001
    }
2002
}
2003

    
2004
#pragma - mark database view delegate
2005

    
2006
- (void)didSetupMappings:(OTRYapViewHandler *)handler
2007
{
2008
    // The databse view is setup now so refresh from there
2009
    [self updateViewWithKey:self.threadKey collection:self.threadCollection];
2010
    [self updateRangeOptions:YES];
2011
    [self.collectionView reloadData];
2012
}
2013

    
2014
- (void)didReceiveChanges:(OTRYapViewHandler *)handler key:(NSString *)key collection:(NSString *)collection
2015
{
2016
    [self updateViewWithKey:key collection:collection];
2017
}
2018

    
2019
- (void)didReceiveChanges:(OTRYapViewHandler *)handler sectionChanges:(NSArray<YapDatabaseViewSectionChange *> *)sectionChanges rowChanges:(NSArray<YapDatabaseViewRowChange *> *)rowChanges
2020
{
2021
    if (!rowChanges.count) {
2022
        return;
2023
    }
2024
    
2025
    // Important to clear our "one message cache" here, since things may have changed.
2026
    self.currentIndexPath = nil;
2027
    
2028
    NSUInteger collectionViewNumberOfItems = [self.collectionView numberOfItemsInSection:0];
2029
    NSUInteger numberMappingsItems = [self.viewHandler.mappings numberOfItemsInSection:0];
2030
    
2031
    [self.collectionView performBatchUpdates:^{
2032
        
2033
        for (YapDatabaseViewRowChange *rowChange in rowChanges)
2034
        {
2035
            switch (rowChange.type)
2036
            {
2037
                case YapDatabaseViewChangeDelete :
2038
                {
2039
                    [self.collectionView deleteItemsAtIndexPaths:@[rowChange.indexPath]];
2040
                    break;
2041
                }
2042
                case YapDatabaseViewChangeInsert :
2043
                {
2044
                    [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
2045
                    break;
2046
                }
2047
                case YapDatabaseViewChangeMove :
2048
                {
2049
                    [self.collectionView moveItemAtIndexPath:rowChange.indexPath toIndexPath:rowChange.newIndexPath];
2050
                    break;
2051
                }
2052
                case YapDatabaseViewChangeUpdate :
2053
                {
2054
                    // Update could be e.g. when we are done auto-loading a link. We
2055
                    // need to reset the stored size of this item, so the image/message
2056
                    // will get the correct bubble height.
2057
                    id <JSQMessageData> message = [self messageAtIndexPath:rowChange.indexPath];
2058
                    [self.collectionView.collectionViewLayout.bubbleSizeCalculator resetBubbleSizeCacheForMessageData:message];
2059
                    [self.messageSizeCache removeObjectForKey:@(message.messageHash)];
2060
                    [self.collectionView reloadItemsAtIndexPaths:@[ rowChange.indexPath]];
2061
                    break;
2062
                }
2063
            }
2064
        }
2065
    } completion:^(BOOL finished){
2066
        if(numberMappingsItems > collectionViewNumberOfItems && numberMappingsItems > 0) {
2067
            //Inserted new item, probably at the end
2068
            //Get last message and test if isIncoming
2069
            NSIndexPath *lastMessageIndexPath = [NSIndexPath indexPathForRow:numberMappingsItems - 1 inSection:0];
2070
            id <OTRMessageProtocol>lastMessage = [self messageAtIndexPath:lastMessageIndexPath];
2071
            if ([lastMessage isMessageIncoming]) {
2072
                [self finishReceivingMessage];
2073
            } else {
2074
                // We can't use finishSendingMessage here because it might
2075
                // accidentally clear out unsent message text
2076
                [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
2077
                [self scrollToBottomAnimated:YES];
2078
            }
2079
        } else {
2080
            [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
2081
        }
2082
    }];
2083
}
2084

    
2085
#pragma - mark UITextViewDelegateMethods
2086

    
2087
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
2088
{
2089
    if ([URL otr_isInviteLink]) {
2090
        NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
2091
        activity.webpageURL = URL;
2092
        [[OTRAppDelegate appDelegate] application:[UIApplication sharedApplication] continueUserActivity:activity restorationHandler:^(NSArray * _Nullable restorableObjects) {
2093
            // TODO: restore stuff
2094
        }];
2095
        return NO;
2096
    }
2097
    
2098
    UIActivityViewController *activityViewController = [UIActivityViewController otr_linkActivityViewControllerWithURLs:@[URL]];
2099
    
2100
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
2101
        activityViewController.popoverPresentationController.sourceView = textView;
2102
        activityViewController.popoverPresentationController.sourceRect = textView.bounds;
2103
    }
2104
    
2105
    [self presentViewController:activityViewController animated:YES completion:nil];
2106
    return NO;
2107
}
2108

    
2109
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
2110
    return NO;
2111
}
2112

    
2113
- (void)viewWillLayoutSubviews {
2114
    self.currentIndexPath = nil;
2115
    [super viewWillLayoutSubviews];
2116
}
2117

    
2118
- (void)viewDidLayoutSubviews {
2119
    [super viewDidLayoutSubviews];
2120
    [self layoutJIDForwardingHeader];
2121
}
2122

    
2123
#pragma - mark Buddy Migration methods
2124

    
2125
- (nullable XMPPJID *)getForwardingJIDForBuddy:(OTRXMPPBuddy *)xmppBuddy {
2126
    XMPPJID *ret = nil;
2127
    if (xmppBuddy != nil && xmppBuddy.vCardTemp != nil) {
2128
        ret = xmppBuddy.vCardTemp.jid;
2129
    }
2130
    return ret;
2131
}
2132

    
2133
- (void)layoutJIDForwardingHeader {
2134
    if (self.jidForwardingHeaderView != nil) {
2135
        [self.jidForwardingHeaderView setNeedsLayout];
2136
        [self.jidForwardingHeaderView layoutIfNeeded];
2137
        int height = [self.jidForwardingHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 1;
2138
        self.jidForwardingHeaderView.frame = CGRectMake(0, self.topLayoutGuide.length, self.view.frame.size.width, height);
2139
        [self.view bringSubviewToFront:self.jidForwardingHeaderView];
2140
        self.topContentAdditionalInset = height;
2141
    }
2142
}
2143

    
2144
- (void)updateJIDForwardingHeader {
2145
    
2146
    __block id<OTRThreadOwner> thread = nil;
2147
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2148
        thread = [self threadObjectWithTransaction:transaction];
2149
    }];
2150
    OTRXMPPBuddy *buddy = nil;
2151
    if ([thread isKindOfClass:[OTRXMPPBuddy class]]) {
2152
        buddy = (OTRXMPPBuddy*)thread;
2153
    }
2154
    
2155
    // If we have a buddy with vcard JID set to something else than the username, show a
2156
    // "buddy has moved" warning to allow the user to start a chat with that JID instead.
2157
    BOOL showHeader = NO;
2158
    XMPPJID *forwardingJid = [self getForwardingJIDForBuddy:buddy];
2159
    if (forwardingJid != nil && ![forwardingJid isEqualToJID:buddy.bareJID options:XMPPJIDCompareBare]) {
2160
        showHeader = YES;
2161
    }
2162
    
2163
    if (showHeader) {
2164
        [self showJIDForwardingHeaderWithNewJID:forwardingJid];
2165
    } else if (!showHeader && self.jidForwardingHeaderView != nil) {
2166
        self.topContentAdditionalInset = 0;
2167
        [self.jidForwardingHeaderView removeFromSuperview];
2168
        self.jidForwardingHeaderView = nil;
2169
    }
2170
}
2171

    
2172
- (void)showJIDForwardingHeaderWithNewJID:(XMPPJID *)newJid {
2173
    if (self.jidForwardingHeaderView == nil) {
2174
        UINib *nib = [UINib nibWithNibName:@"MigratedBuddyHeaderView" bundle:OTRAssets.resourcesBundle];
2175
        MigratedBuddyHeaderView *header = (MigratedBuddyHeaderView*)[nib instantiateWithOwner:self options:nil][0];
2176
        [header setForwardingJID:newJid];
2177
        [header.titleLabel setText:MIGRATED_BUDDY_STRING()];
2178
        [header.descriptionLabel setText:MIGRATED_BUDDY_INFO_STRING()];
2179
        [header.switchButton setTitle:MIGRATED_BUDDY_SWITCH() forState:UIControlStateNormal];
2180
        [header.ignoreButton setTitle:MIGRATED_BUDDY_IGNORE() forState:UIControlStateNormal];
2181
        [header setBackgroundColor:UIColor.whiteColor];
2182
        [self.view addSubview:header];
2183
        [self.view bringSubviewToFront:header];
2184
        self.jidForwardingHeaderView = header;
2185
        [self.view setNeedsLayout];
2186
    }
2187
}
2188

    
2189
- (IBAction)didPressMigratedIgnore {
2190
    if (self.jidForwardingHeaderView != nil) {
2191
        self.jidForwardingHeaderView.hidden = YES;
2192
        self.topContentAdditionalInset = 0;
2193
    }
2194
}
2195

    
2196
- (IBAction)didPressMigratedSwitch {
2197
    if (self.jidForwardingHeaderView != nil) {
2198
        self.jidForwardingHeaderView.hidden = YES;
2199
        self.topContentAdditionalInset = 0;
2200
    }
2201
    
2202
    __block OTRXMPPBuddy *buddy = nil;
2203
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2204
        buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction];
2205
    }];
2206
    
2207
    XMPPJID *forwardingJid = [self getForwardingJIDForBuddy:buddy];
2208
    if (forwardingJid != nil) {
2209
        // Try to find buddy
2210
        //
2211
        [[OTRDatabaseManager sharedInstance].readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
2212
            OTRAccount *account = [self accountWithTransaction:transaction];
2213
            OTRXMPPBuddy *buddy = [OTRXMPPBuddy fetchBuddyWithJid:forwardingJid accountUniqueId:account.uniqueId transaction:transaction];
2214
            if (!buddy) {
2215
                buddy = [[OTRXMPPBuddy alloc] init];
2216
                buddy.accountUniqueId = account.uniqueId;
2217
                buddy.username = forwardingJid.bare;
2218
                [buddy saveWithTransaction:transaction];
2219
                id<OTRProtocol> proto = [[OTRProtocolManager sharedInstance] protocolForAccount:account];
2220
                if (proto != nil) {
2221
                    [proto addBuddy:buddy];
2222
                }
2223
            }
2224
            [self setThreadKey:buddy.uniqueId collection:[OTRBuddy collection]];
2225
        }];
2226
    }
2227
}
2228

    
2229
#pragma - mark Group chat support
2230

    
2231
- (void)setupWithBuddies:(NSArray<NSString *> *)buddies accountId:(NSString *)accountId name:(NSString *)name
2232
{
2233
    __block OTRXMPPAccount *account = nil;
2234
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2235
        account = [OTRXMPPAccount fetchObjectWithUniqueID:accountId transaction:transaction];
2236
    }];
2237
    OTRXMPPManager *xmppManager = (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
2238
    NSString *service = [xmppManager.roomManager.conferenceServicesJID firstObject];
2239
    if (service.length > 0) {
2240
        NSString *roomName = [NSUUID UUID].UUIDString;
2241
        XMPPJID *roomJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@",roomName,service]];
2242
        self.threadKey = [xmppManager.roomManager startGroupChatWithBuddies:buddies roomJID:roomJID nickname:account.displayName subject:name];
2243
        [self setThreadKey:self.threadKey collection:[OTRXMPPRoom collection]];
2244
    } else {
2245
        DDLogError(@"No conference server for account: %@", account.username);
2246
    }
2247
}
2248

    
2249
#pragma - mark OTRRoomOccupantsViewControllerDelegate
2250

    
2251
- (void)didLeaveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2252
    __block OTRXMPPRoom *room = nil;
2253
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2254
        room = [self roomWithTransaction:transaction];
2255
    }];
2256
    if (room) {
2257
        [self setThreadKey:nil collection:nil];
2258
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2259
            [room removeWithTransaction:transaction];
2260
        }];
2261
    }
2262
    [self.navigationController popViewControllerAnimated:NO];
2263
    if ([[self.navigationController viewControllers] count] > 1) {
2264
        [self.navigationController popViewControllerAnimated:YES];
2265
    } else {
2266
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2267
    }
2268
    
2269
}
2270

    
2271
- (void)didArchiveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2272
    __block OTRXMPPRoom *room = nil;
2273
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2274
        room = [self roomWithTransaction:transaction];
2275
    }];
2276
    if (room) {
2277
        [self setThreadKey:nil collection:nil];
2278
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2279
            room.isArchived = YES;
2280
            [room saveWithTransaction:transaction];
2281
        }];
2282
    }
2283
    [self.navigationController popViewControllerAnimated:NO];
2284
    if ([[self.navigationController viewControllers] count] > 1) {
2285
        [self.navigationController popViewControllerAnimated:YES];
2286
    } else {
2287
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2288
    }
2289
}
2290
@end